package net.heavencraft.bejeweled;

import java.awt.Point;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Random;

import net.heavencraft.bejeweled.jewel.Jewel;
import net.heavencraft.bejeweled.jewel.JewelColor;

/**
 * Represents a GameGrid, a container for the jewels
 * 
 * @author Sven
 * @author Wesley
 * 
 */
public class GameGrid implements Drawable {

	/**
	 * Storage for the jewels
	 */
	private Jewel[][] grid;

	/**
	 * Reference to the main class
	 */
	private final Bejeweled main;

	/**
	 * Location of this grid
	 */
	private Point location;

	/**
	 * Creates a new empty GameGrid
	 * 
	 * @param main
	 *            Reference to the main class
	 * @param width
	 *            The ammount of horizontal Gems
	 * @param height
	 *            The ammount of vertical Gems
	 */
	public GameGrid(Bejeweled main, int width, int height, Point location) {
		this.location = location;
		this.main = main;
		grid = new Jewel[width][height];
	}

	/**
	 * Gets the object in a specific grid-coord
	 * 
	 * @param x
	 *            The x of the grid
	 * @param y
	 *            The y of the grid
	 * @return The object at this location
	 */
	public Jewel getObjectAt(int x, int y) {

		try {
			return grid[x][y];
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * Sets the Jewel at the specified location
	 * 
	 * @param obj
	 *            The object that will be set
	 * @param x
	 *            The x of the grid
	 * @param y
	 *            The y of the grid
	 */
	public void setJewelAt(Jewel obj, int x, int y) {
		grid[x][y] = obj;
	}

	/**
	 * Sets null on the specified location
	 * 
	 * @param x
	 *            The x of the grid
	 * @param y
	 *            The y of the grid
	 */
	public void removeJewelAt(int x, int y) {
		setJewelAt(null, x, y);
	}

	/**
	 * Sets null on the specified location
	 * 
	 * @param loc
	 *            The location of the jewel
	 */
	public void removeJewelAt(Point loc) {
		setJewelAt(null, loc.x, loc.y);
	}

	/**
	 * Removes a specified jewel
	 * 
	 * @param obj
	 *            The jewel that will be removed.
	 */
	public void removeJewel(Jewel obj) {
		if (getLocation(obj) == null)
			return;
		removeJewelAt(getLocation(obj));
	}

	/**
	 * Gets a list of all objects contained in this grid
	 * 
	 * @return A list of Jewels currently in this grid
	 */
	public List<Jewel> getObjects() {
		List<Jewel> objs = new ArrayList<Jewel>();
		for (int i = 0; i < grid.length; i++) {
			for (int j = 0; j < grid[0].length; j++) {
				if (grid[i][j] == null)
					continue;
				objs.add(grid[i][j]);
			}
		}
		return objs;
	}

	/**
	 * Gets the location object of the specified jewel
	 * 
	 * @param o
	 *            The jewel to get the location of
	 * @return The location of the specified jewel.
	 */
	public Point getLocation(Jewel o) {
		for (int i = 0; i < Bejeweled.jewels; i++)
			for (int j = 0; j < Bejeweled.jewels; j++)
				if (grid[i][j] != null && grid[i][j].equals(o))
					return new Point(i, j);
		return null;
	}

	/**
	 * Empties the grid by creating a new 2-dimensional array
	 */
	public void clearGrid() {
		grid = new Jewel[Bejeweled.jewels][Bejeweled.jewels];
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.heavencraft.bejeweled.Drawable#draw()
	 */
	@Override
	public void draw() {
		for (Jewel obj : getObjects()) {
			if (obj != null)
				obj.draw();
		}
	}

	/**
	 * Generates a new randomly-filled grid
	 */
	public void generateGrid() {
		Random r = new Random();
		int matchCountX;
		JewelColor type, newType;
		JewelColor[] colors = EnumSet.allOf(JewelColor.class).toArray(
				new JewelColor[0]);

		clearGrid();

		for (int x = 0; x < Bejeweled.jewels; x++) {
			int loopCounter;
			matchCountX = 0;
			type = null;
			newType = null;
			for (int y = 0; y < Bejeweled.jewels; y++) {
				loopCounter = 0;
				newType = colors[r.nextInt(colors.length)];
				if (newType == type) {
					matchCountX++;
					if (matchCountX == 2) {
						while (newType == type) {
							newType = colors[r.nextInt(colors.length)];
						}
					}
				} else {
					matchCountX = 0;
				}

				Jewel objectY1 = (x > 1 ? getObjectAt(x - 1, y) : null);
				Jewel objectY2 = (x > 2 ? getObjectAt(x - 2, y) : null);
				if (objectY1 != null && objectY2 != null) {

					if (objectY1.getColor() == objectY2.getColor()) {
						if (objectY1.getColor() == newType) {
							while (objectY1.getColor() == newType
									|| newType == type) {
								newType = colors[r.nextInt(colors.length)];
								loopCounter++;
								newType = colors[r.nextInt(colors.length)];
								// Prevent Deadlocks :$
								if (loopCounter > 100) {
									clearGrid();
									generateGrid();
									return;
								}
							}
						}
					}
				}
				Jewel obj = new Jewel(main, newType);
				setJewelAt(obj, x, y);
				type = newType;
				newType = null;
			}
		}
	}

	/**
	 * Sets all the jewels in the grid to non-selected state
	 */
	public void unselectAllObjects() {
		for (Jewel o : getSelectedObjects()) {
			o.toggleSelected();
		}
	}

	/**
	 * Returns an array of objects around an object
	 * 
	 * @param obj
	 *            The object to check for neighbors
	 * @return An array of neighbors of the specified Jewel
	 */
	public Jewel[] getObjectsAround(Jewel obj) {

		ArrayList<Jewel> objects = new ArrayList<Jewel>();
		Point p = getLocation(obj);
		int x = p.x;
		int y = p.y;

		if (x > 0)
			objects.add(getObjectAt(x - 1, y)); // LEFT
		if (x < Bejeweled.jewels - 1)
			objects.add(getObjectAt(x + 1, y)); // RIGHT
		if (y > 0)
			objects.add(getObjectAt(x, y - 1)); // TOP
		if (y < Bejeweled.jewels - 1)
			objects.add(getObjectAt(x, y + 1)); // BOTTOM
		return objects.toArray(new Jewel[0]);
	}

	/**
	 * Gets all the jewels around the specified jewel in a 3x3 area, excluding
	 * itself
	 * 
	 * @param jewel
	 *            The jewel to check for surrounding jewels
	 * @return Surrounding jewels
	 */
	public Jewel[] get3x3(Jewel jewel) {
		ArrayList<Jewel> objects = new ArrayList<>();

		for (int i = -1; i < 2; i++) {
			for (int j = -1; j < 2; j++) {
				Point p = getLocation(jewel);
				Jewel jwl = getObjectAt(p.x + i, p.y + j);
				if (jwl != null && jwl.equals(jewel)) {
					objects.add(jwl);
				}
			}
		}
		return objects.toArray(new Jewel[0]);
	}

	/**
	 * Gets the object at the specified screenlocation
	 * 
	 * @param mouseX
	 *            The x location
	 * @param mouseY
	 *            The y location
	 * @return The object at this location, null if no object or out of bounds
	 */
	public Jewel getObjectAtLocation(int mouseX, int mouseY) {
		int x = (int) Math.floor((mouseX - getX()) / Bejeweled.jewelSize);
		int y = (int) Math.floor((mouseY - getY()) / Bejeweled.jewelSize);
		if (mouseX < getX())
			x--;
		if (mouseY < getY())
			y--;
		// System.out.println(x + " : " + y);
		if (x > Bejeweled.jewels - 1 || y > Bejeweled.jewels - 1)
			return null;
		else
			return getObjectAt(x, y);

	}

	/**
	 * Gets the screen X coordinate of the grid
	 * 
	 * @return The x Location
	 */
	public int getX() {
		return (int) location.getX();
	}

	/**
	 * Gets the screen Y coordinate of the grid
	 * 
	 * @return The y Location
	 */
	public int getY() {
		return (int) location.getY();
	}

	/**
	 * Swaps the position of the two specified jewels
	 * 
	 * @param a
	 *            The first jewel
	 * @param b
	 *            The second jewel
	 */
	public void switchObjects(Jewel a, Jewel b) {
		Point locA = getLocation(a);
		Point locB = getLocation(b);
		setJewelAt(b, locA.x, locA.y);
		setJewelAt(a, locB.x, locB.y);

		List<List<Point>> stackY = findStacksY();
		List<List<Point>> stackX = findStacksX();
		boolean found = false;
		for (List<Point> list : stackY) {
			if (list.contains(locA) || list.contains(locB)) {
				found = true;
				// ....up and down
			}
		}

		for (List<Point> list : stackX) {
			if (list.contains(locA) || list.contains(locB)) {
				found = true;
				// ....left and right
			}
		}
		if (found) {
			main.mapChanged();
		} else {
			setJewelAt(a, locA.x, locA.y);
			setJewelAt(b, locB.x, locB.y);
		}
	}

	/**
	 * Called to see if there is another streak
	 */
	private void keepStreak() {
		if (findStacksX().size() > 0 || findStacksY().size() > 0) {
			main.mapChanged();
		}
	}

	/**
	 * Gets a list of all selected jewels
	 * 
	 * @return All selected jewels
	 */
	public List<Jewel> getSelectedObjects() {
		List<Jewel> objs = new ArrayList<Jewel>();
		for (Jewel o : getObjects()) {
			if (o.isSelected())
				objs.add(o);
		}
		return objs;
	}

	/**
	 * Cascades all the jewels so that they fill up the spots underneath them.
	 */
	public void cascade() {
		for (int x = 0; x < grid.length; x++) {
			while (true) {
				for (int y = grid[0].length - 1; y >= 0; y--) {
					System.out.println(x + " : " + y);
					if (grid[x][y] == null) {
						if (y > 0) {
							grid[x][y] = grid[x][y - 1];
							grid[x][y - 1] = null;
						}
					}
				}
				if (grid[x][0] == null) {
					grid[x][0] = new Jewel(main, JewelColor.getRandom());
				} else {
					break;
				}
			}
		}
		keepStreak();
	}

	/**
	 * Gets all the streaks on the Horizontal grid
	 * 
	 * @return All streaks found
	 */
	public List<List<Point>> findStacksX() {
		JewelColor type = null;
		List<List<Point>> stacks = new ArrayList<>();
		for (int y = 0; y < grid[0].length; y++) {
			for (int x = 0; x < grid.length; x++) {
				if (getObjectAt(x, y) == null) {
					continue;
				}
				List<Point> stacknode = new ArrayList<>();
				type = getObjectAt(x, y).getColor();
				stacknode.add(new Point(x, y));
				while (true) {
					if (x + 1 >= grid.length)
						break;
					if (getObjectAt(x + 1, y) == null)
						break;
					if (getObjectAt(x + 1, y).getColor() == type) {
						stacknode.add(new Point(x + 1, y));
						x++;
					} else {
						break;
					}
				}
				if (stacknode.size() >= 3) {
					stacks.add(stacknode);
				}
			}
		}
		return stacks;
	}

	/**
	 * Gets all the streaks on the Horizontal grid
	 * 
	 * @return All streaks found
	 */
	public List<List<Point>> findStacksY() {
		JewelColor type = null;
		List<List<Point>> stacks = new ArrayList<>();
		for (int x = 0; x < grid.length; x++) {
			for (int y = 0; y < grid[0].length; y++) {
				if (getObjectAt(x, y) == null) {
					continue;
				}
				List<Point> stacknode = new ArrayList<>();
				stacknode.add(new Point(x, y));
				type = getObjectAt(x, y).getColor();
				while (true) {
					if (y + 1 >= grid[0].length)
						break;
					if (getObjectAt(x, y + 1) == null) {
						break;
					}
					if (getObjectAt(x, y + 1).getColor() == type) {
						stacknode.add(new Point(x, y + 1));
						y++;
					} else {
						break;
					}
				}
				if (stacknode.size() >= 3) {
					stacks.add(stacknode);
				}
			}
		}
		return stacks;
	}

	/**
	 * Gets all the jewels of the specified color
	 * 
	 * @param jewel
	 *            The color of the jewel to check for
	 * @return All the jewels of the same color as the input
	 */
	public Jewel[] getJewelsOfColor(JewelColor jewel) {
		List<Jewel> jewels = new ArrayList<>();
		for (Jewel j : this.getObjects()) {
			if (j.getColor() == jewel) {
				jewels.add(j);
			}
		}
		return jewels.toArray(new Jewel[0]);
	}
}
